/*
 * Copyright 2007-2008 Andrew O'Malley
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package ws.quokka.plugin.repository;

import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.taskdefs.Jar;

import ws.quokka.core.bootstrap_util.Assert;
import ws.quokka.core.bootstrap_util.IOUtils;
import ws.quokka.core.metadata.Metadata;
import ws.quokka.core.metadata.MetadataAware;
import ws.quokka.core.repo_spi.RepoArtifact;
import ws.quokka.core.repo_spi.RepoArtifactId;
import ws.quokka.core.repo_spi.RepoDependency;
import ws.quokka.core.repo_spi.RepoOverride;
import ws.quokka.core.repo_spi.RepoPathSpec;
import ws.quokka.core.repo_spi.RepoType;
import ws.quokka.core.repo_spi.RepoXmlConverter;
import ws.quokka.core.repo_spi.Repository;
import ws.quokka.core.repo_spi.RepositoryAware;
import ws.quokka.core.repo_spi.RepositoryFactory;
import ws.quokka.core.repo_spi.RepositoryFactoryAware;
import ws.quokka.core.repo_spi.UnresolvedArtifactException;
import ws.quokka.core.util.Strings;
import ws.quokka.core.version.Version;

import ws.quokka.plugin.lifecycle.LifeCycleImpl;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;

import java.text.DateFormat;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;


/**
 *
 */
public class RepositoryPlugin extends LifeCycleImpl implements MetadataAware, RepositoryAware, RepositoryFactoryAware {
    //~ Static fields/initializers -------------------------------------------------------------------------------------

    public static final String LS = System.getProperty("line.separator");

    //~ Instance fields ------------------------------------------------------------------------------------------------

    private Metadata metadata;
    private Repository repository;
    private RepositoryFactory repositoryFactory;
    private Writer writer;

    //~ Methods --------------------------------------------------------------------------------------------------------

    public void setRepository(Repository repository) {
        this.repository = repository;
    }

    public void setMetadata(Metadata metadata) {
        this.metadata = metadata;
    }

    public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
        this.repositoryFactory = repositoryFactory;
    }

    public void packageIt() {
        String artifactName = properties().getString("artifactName");
        boolean multiple = artifactName.indexOf(",") == -1;
        assertTrue(multiple, "'artifactName' property is mandatory when there are multiple jar artifacts");

        RepoArtifactId outId = metadata.getArtifactId(artifactName, "jar");
        String pathId = properties().getString("path", multiple ? null : "bundle");
        assertTrue(pathId != null, "'path' property is mandatory when there are multiple jar artifacts");

        List path = metadata.getProjectPath(pathId);

        // Create an exploded file repository containing the artifacts in the path
        File repositoryRoot = new File(getLifeCycle().getPackageOutput(), artifactName);
        utils().mkdir(repositoryRoot);

        for (Iterator i = path.iterator(); i.hasNext();) {
            RepoArtifact artifact = (RepoArtifact)i.next();
            copyArtifact(artifact, repositoryRoot);
        }

        Jar jar = (Jar)getProject().createTask("jar");
        jar.setDestFile(getPackageFile(outId));
        jar.setBasedir(repositoryRoot);
        jar.perform();
    }

    private void copyArtifact(RepoArtifact artifact, File repositoryRoot) {
        RepoArtifactId id = artifact.getId();
        RepoType type = metadata.getType(id.getType());
        String artifactRoot = id.getGroup() + "_" + id.getVersion() + "_" + id.getName() + "_" + type.getId();

        // Copy the artifact
        File artifactFile = new File(repositoryRoot, artifactRoot + "." + type.getExtension());
        Copy copy = (Copy)utils().init(new Copy(), "copy");
        copy.setFile(artifact.getLocalCopy());
        copy.setTofile(artifactFile);
        copy.perform();

        // Serialise the metadata to XML
        // TODO: Only getOrCreate if metadata is modified?
        File repoFile = new File(repositoryRoot, artifactRoot + "_repository.xml");
        RepoXmlConverter.toXml(artifact, repoFile);
    }

    private File getPackageFile(RepoArtifactId id) {
        return new File(getLifeCycle().getPackageOutput(),
            id.toPathString() + "." + metadata.getType(id.getType()).getExtension());
    }

    public void updateSnapshots() {
        Set dependencies = metadata.getDependencies();

        for (Iterator i = dependencies.iterator(); i.hasNext();) {
            RepoArtifactId id = (RepoArtifactId)i.next();

            if (id.getVersion().isSnapShot()) {
                log().verbose("Attempting to update snapshot for " + id.toShortString());

                RepoArtifact artifact = repository.resolve(id);
                RepoArtifact updated = repository.updateSnapshot(artifact);

                if (updated != null) {
                    log().info("Updating snapshot " + id.toShortString() + " from " + format(artifact.getTimestamp())
                        + " to " + format(updated.getTimestamp()));
                    repository.install(updated);
                }
            }
        }
    }

    private String format(Date date) {
        if (date == null) {
            return "null";
        }

        DateFormat format = DateFormat.getDateTimeInstance();

        return format.format(date);
    }

    public void list() throws IOException {
        Repository repo = repository;
        String repositoryId = properties().getString("repository", null);
        String format = properties().getString("format", null);
        File output = properties().getFile("output", null);
        String idString = properties().getString("id", null);
        format = (format != null) ? format : ((idString == null) ? "short" : "long");

        RepoArtifactId id = null;

        if (idString != null) {
            id = RepoArtifactId.parse(idString);

            if (id.getVersion() != null) {
                id = id.mergeDefaults();
            }
        }

        try {
            if (output != null) {
                File parent = output.getParentFile();
                Assert.isTrue(parent.exists() || parent.mkdirs(), "Cannot create " + parent.getPath());
                writer = new BufferedWriter(new FileWriter(output));
            }

            boolean includeReferenced = true;

            if (repositoryId != null) {
                repo = repositoryFactory.getOrCreate(repositoryId, true);
                includeReferenced = properties().getBoolean("includeReferenced", false);
            }

            Collection ids;

            if (id == null) {
                ids = repo.listArtifactIds(includeReferenced);
            } else {
                if (id.getVersion() == null) {
                    ids = repo.listArtifactIds(id.getGroup(), id.getName(), id.getType(), includeReferenced);
                } else {
                    ids = Collections.singleton(id);
                }
            }

            if (format.equals("short")) {
                listShort(repo, ids);
            } else if (format.equals("ids")) {
                listIds(ids);
            } else if (format.equals("long")) {
                listLong(repo, ids);
            } else {
                Assert.isTrue(false, "Unknown format: " + format);
            }
        } finally {
            if (writer != null) {
                writer.close();
            }
        }

        log().info("For additional formatting, filtering and ouput options see the help:");
        log().info("   $ quokka help -Dtarget=list");
    }

    private void listIds(Collection ids) throws IOException {
        ids = new TreeSet(ids);

        for (Iterator i = ids.iterator(); i.hasNext();) {
            RepoArtifactId id = (RepoArtifactId)i.next();
            writeln(id.toShortString());
        }
    }

    private void listLong(Repository repo, Collection ids)
            throws IOException {
        ids = new TreeSet(ids);

        String lastGroup = null;

        for (Iterator i = ids.iterator(); i.hasNext();) {
            RepoArtifactId id = (RepoArtifactId)i.next();

            RepoArtifact artifact = repo.resolve(id, false);
            String grouping = id.getGroup() + "_" + id.getVersion();

            if (!grouping.equals(lastGroup)) {
                writeln(id.getGroup() + " " + id.getVersion());
            }

            lastGroup = grouping;

            writeln("    " + Strings.pad(id.getName(), 15, ' ') + " " + Strings.pad(id.getType(), 10, ' ') + " "
                + ((artifact.getShortDescription() == null) ? "" : artifact.getShortDescription()));

            if (artifact.getLocalCopy() != null) {
                String path = artifact.getLocalCopy().getPath();
                writeln("        local copy: " + path);

                writeEditorHint("", path);
            }

            writeln("          licenses: " + formatLicenses(artifact));

            if (artifact.getDependencies().size() != 0) {
                writeln("      dependencies:");
                writeln(formatDependencies(artifact.getDependencies()));
            }

            if (artifact.getOverrides().size() != 0) {
                writeln("         overrides:");
                writeln(formatOverrides(artifact.getOverrides()));
            }

            writeln(" ");
        }
    }

    private void writeEditorHint(String prefix, String path)
            throws IOException {
        String editor = getProjectProperties().getString("editor", null);

        if (editor != null) {
            path = path.substring(0, path.lastIndexOf('.')) + "_repository.xml";
            writeln(prefix + editor + " " + path);
        }
    }

    private void listShort(Repository repository, Collection ids)
            throws IOException {
        Set sorted = new TreeSet(new Comparator() {
                    public int compare(Object o1, Object o2) {
                        RepoArtifactId lhs = (RepoArtifactId)o1;
                        RepoArtifactId rhs = (RepoArtifactId)o2;
                        int result = lhs.getGroup().compareTo(rhs.getGroup());
                        result = (result != 0) ? result : lhs.getName().compareTo(rhs.getName());
                        result = (result != 0) ? result : lhs.getType().compareTo(rhs.getType());
                        result = (result != 0) ? result : lhs.getVersion().compareTo(rhs.getVersion());

                        return result;
                    }
                });
        sorted.addAll(ids);

        Set versions = new TreeSet();
        RepoArtifactId last = null;

        for (Iterator i = sorted.iterator(); i.hasNext();) {
            RepoArtifactId id = (RepoArtifactId)i.next();

            if (!id.toUnversionedId().equals(last)) {
                if (last != null) {
                    formatShort(repository, last, versions);
                }

                versions.clear();
                last = id.toUnversionedId();
            }

            versions.add(id.getVersion());
        }

        if (last != null) {
            formatShort(repository, last, versions);
        }
    }

    private void formatShort(Repository repository, RepoArtifactId id, Set versions)
            throws IOException {
        String idString = id.getGroup() + ":" + (id.isDefaultName() ? "" : id.getName()) + ":" + id.getType();
        writeln(Strings.pad(idString, 39, ' ') + " " + Strings.join(versions.iterator(), " "));

        Version version = null;

        for (Iterator i = versions.iterator(); i.hasNext();) {
            version = (Version)i.next();
        }

        RepoArtifact artifact = repository.resolve(new RepoArtifactId(id.getGroup(), id.getName(), id.getType(), version),
                false);

        if (artifact.getDescription() != null) {
            writeln("    " + artifact.getShortDescription());
        }

        if (artifact.getLocalCopy() != null) {
            writeEditorHint("    ", artifact.getLocalCopy().getPath());
        }

        writeln("");
    }

    private void writeln(String message) throws IOException {
        if (writer != null) {
            writer.write(message + LS);
        } else {
            // Ant's logging trims messages and discards if blank.
            // This hack inserts a unprintable DELETE character that won't get trimmed, allowing a blank line to be logged
            // TODO: find a better way ...
            if ("".equals(message.trim())) {
                message = "\u007f";
            }

            getProject().log(message);
        }
    }

    private String formatOverrides(List overrides) {
        StringBuffer sb = new StringBuffer();

        for (Iterator i = overrides.iterator(); i.hasNext();) {
            RepoOverride override = (RepoOverride)i.next();
            sb.append(override.getGroup()).append(":");
            sb.append(override.getName()).append(":");
            sb.append(override.getType()).append(":");
            sb.append(override.getVersion());
            sb.append(" with ");

            if (override.getWithVersion() != null) {
                sb.append(override.getWithVersion());

                if (override.getWithPathSpecs().size() != 0) {
                    sb.append(", ");
                }
            }

            for (Iterator j = override.getWithPathSpecs().iterator(); j.hasNext();) {
                RepoPathSpec pathSpec = (RepoPathSpec)j.next();
                sb.append(pathSpec.toShortHand());

                if (j.hasNext()) {
                    sb.append(", ");
                }
            }
        }

        sb.append(LS);

        return sb.toString();
    }

    private String formatDependencies(Set dependencies) {
        StringBuffer sb = new StringBuffer();
        int[] widths = new int[3];

        for (Iterator i = dependencies.iterator(); i.hasNext();) {
            RepoDependency dependency = (RepoDependency)i.next();
            RepoArtifactId id = dependency.getId();
            widths[0] = Math.max(widths[0], id.getGroup().length());
            widths[1] = Math.max(widths[1], id.getName().length());
            widths[2] = Math.max(widths[2], id.getVersion().toString().length());
        }

        SortedSet sorted = new TreeSet(new Comparator() {
                    public int compare(Object o1, Object o2) {
                        return ((RepoDependency)o1).getId().compareTo(((RepoDependency)o2).getId());
                    }
                });
        sorted.addAll(dependencies);

        for (Iterator i = sorted.iterator(); i.hasNext();) {
            RepoDependency dependency = (RepoDependency)i.next();
            RepoArtifactId id = dependency.getId();
            sb.append("            ");
            sb.append(Strings.pad(id.getGroup(), widths[0] + 1, ' '));
            sb.append(Strings.pad(id.getName(), widths[1] + 1, ' '));
            sb.append(Strings.pad(id.getVersion().toString(), widths[2], ' '));
            sb.append(": ");

            for (Iterator j = dependency.getPathSpecs().iterator(); j.hasNext();) {
                RepoPathSpec pathSpec = (RepoPathSpec)j.next();
                sb.append(pathSpec.toShortHand());

                if (j.hasNext()) {
                    sb.append(", ");
                }
            }

            sb.append(LS);
        }

        return sb.toString();
    }

    private StringBuffer formatLicenses(RepoArtifact artifact) {
        // Licenses
        StringBuffer sb = new StringBuffer();

        for (Iterator j = artifact.getLicenses().iterator(); j.hasNext();) {
            RepoArtifactId license = (RepoArtifactId)j.next();
            sb.append(license.toShortString());

            if (j.hasNext()) {
                sb.append(", ");
            }
        }

        return sb;
    }

    public void copy() {
        String fromId = properties().getString("from", null);
        Repository from = (fromId == null) ? this.repository : repositoryFactory.getOrCreate(fromId, true);
        Repository to = repositoryFactory.getOrCreate(properties().getString("to"), true);

        for (Iterator i = getIds().iterator(); i.hasNext();) {
            RepoArtifactId id = (RepoArtifactId)i.next();
            RepoArtifact artifact = from.resolve(id);
            to.install(artifact);
        }
    }

    public void copyPath() {
        Repository to = repositoryFactory.getOrCreate(properties().getString("to"), true);

        for (Iterator i = metadata.getProjectPath(properties().getString("path")).iterator(); i.hasNext();) {
            RepoArtifact artifact = (RepoArtifact)i.next();
            to.install(artifact);
        }
    }

    public void remove() {
        String fromId = properties().getString("from", null);
        Repository from = (fromId == null) ? this.repository : repositoryFactory.getOrCreate(fromId, true);

        for (Iterator i = getIds().iterator(); i.hasNext();) {
            RepoArtifactId id = (RepoArtifactId)i.next();
            from.remove(id);
        }
    }

    public void install() {
        // Validate parameters
        RepoArtifactId id = getId();
        File file = properties().getFile("file");
        Assert.isTrue(file.exists(), file.getPath() + " does not exist");

        String toId = properties().getString("to", null);
        Repository to = (toId == null) ? this.repository : repositoryFactory.getOrCreate(toId, true);

        // Check if it already exists and if it does whether it's a stub
        RepoArtifact artifact = null;

        try {
            artifact = to.resolve(id);
            Assert.isTrue((artifact.getLocalCopy() == null) && artifact.isStub(),
                "Cannot install as the artifact already exists in the repository");
        } catch (UnresolvedArtifactException e) {
            // Ingore
        }

        // Install the artifact with the file provided
        artifact = (artifact != null) ? artifact : new RepoArtifact(id);
        artifact.setLocalCopy(file);
        to.install(artifact);

        artifact = to.resolve(id);

        String location = artifact.getLocalCopy().getPath();
        log().info("   Artifact file copied to: " + location);
        log().info("Repository file written to: " + location.substring(0, location.lastIndexOf("."))
            + "_repository.xml");
    }

    private RepoArtifactId getId() {
        String idString = properties().getString("id", null);

        if (idString == null) {
            return null;
        }

        RepoArtifactId id = RepoArtifactId.parse(idString).mergeDefaults();
        id.validate();

        return id;
    }

    private List getIds() {
        RepoArtifactId id = getId();
        File ids = properties().getFile("ids", null);
        Assert.isTrue(((id != null) && (ids == null)) || ((id == null) && (ids != null)),
            "Either the 'id' or 'ids' properties must be set, but not both");

        if (id != null) {
            return Collections.singletonList(id);
        } else {
            Assert.isTrue(ids.exists(), "Ids file does not exist: " + ids.getPath());

            return parseIds(ids);
        }
    }

    private List parseIds(File idsFile) {
        List idStrings = Arrays.asList(Strings.trim(Strings.split(new IOUtils().fileToString(idsFile), "\n\r")));
        List ids = new ArrayList();

        for (Iterator i = idStrings.iterator(); i.hasNext();) {
            String idString = (String)i.next();

            if (!idString.equals("")) {
                RepoArtifactId id = RepoArtifactId.parse(idString);
                id = id.mergeDefaults();
                id.validate();
                ids.add(id);
            }
        }

        return ids;
    }

    public void resolveDependencies() {
        metadata.getDependencies();
    }

    public void rebuildCaches() {
        String repositoryId = properties().getString("repository", null);
        Repository repository = (repositoryId == null) ? this.repository
                                                       : repositoryFactory.getOrCreate(repositoryId, true);
        repository.rebuildCaches();
    }
}
